// Copyright 2014 The Go Circuit Project
// Use of this source code is governed by the license for
// The Go Circuit Project, found in the LICENSE file.
//
// Authors:
//   2014 Chris Monson <chris@gocircuit.org>

package ripple

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math/rand"
	"os"
	"os/exec"
	"path"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"syscall"

	"circuit/kit/iomisc"
	"circuit/kit/rh"
	"circuit/kit/rh/ns"
	"circuit/kit/rh/shargs"
)

var (
	ctrlExp   = regexp.MustCompile(`^\s*(kill|start|reset)(?:\s+-?(?:(\d+)|(\w+)))?\s*$`)
	sigStrMap = map[string]syscall.Signal{
		"ABRT":   syscall.SIGABRT,
		"ALRM":   syscall.SIGALRM,
		"BUS":    syscall.SIGBUS,
		"CHLD":   syscall.SIGCHLD,
		"CONT":   syscall.SIGCONT,
		"FPE":    syscall.SIGFPE,
		"HUP":    syscall.SIGHUP,
		"ILL":    syscall.SIGILL,
		"INT":    syscall.SIGINT,
		"IO":     syscall.SIGIO,
		"IOT":    syscall.SIGIOT,
		"KILL":   syscall.SIGKILL,
		"PIPE":   syscall.SIGPIPE,
		"PROF":   syscall.SIGPROF,
		"QUIT":   syscall.SIGQUIT,
		"SEGV":   syscall.SIGSEGV,
		"STOP":   syscall.SIGSTOP,
		"SYS":    syscall.SIGSYS,
		"TERM":   syscall.SIGTERM,
		"TRAP":   syscall.SIGTRAP,
		"TSTP":   syscall.SIGTSTP,
		"TTIN":   syscall.SIGTTIN,
		"TTOU":   syscall.SIGTTOU,
		"URG":    syscall.SIGURG,
		"USR1":   syscall.SIGUSR1,
		"USR2":   syscall.SIGUSR2,
		"VTALRM": syscall.SIGVTALRM,
		"WINCH":  syscall.SIGWINCH,
		"XCPU":   syscall.SIGXCPU,
		"XFSZ":   syscall.SIGXFSZ,
	}
)

type RunState int

const (
	Unknown RunState = iota
	None
	Running
	Exited
	Stopped
	Signaled
	Continued
)

func buildProcHelp(name string, id uint64) string {
	return fmt.Sprintf(procHelpFmt, path.Join("/", name), id)
}

//  ▒ fd stdin stdout stderr ············ Forward the process' descriptors to this console.

const procHelpFmt = `
  This is a PROC ripple, named "%s", with QID ⟨Q%016x⟩.

  SYNOPSIS

  cat help                   # Read this help screen.
  cat stat                   # Read a description of this process' status.

  Before starting the process, you can prepare it by writing a shell-like
  command to "spec_quoted", thus:

  $ echo 'MYVAR=hello /bin/bash -c "echo $MYVAR"' >> spec_quoted

  Note that echo is using single quotes, to ensure that the shell does not
  interpret the contents. Thus the interior double quotes are passed to and
  interpreted by the quoted spec parser, which uses spaces as a separator and
  treats quoted entities as units.

  The above will create a process with

  env:
    MYVAR=hello
  bin:
    /bin/bash
  arg:
    -c
    echo $MYVAR

  You can also see this via

  $ cat spec_json
  {
    "env": [
      "MYVAR=hello"
    ],
    "bin": "/bin/bash",
    "arg": [
      "-c",
      "echo $MYVAR"
    ]
  }

  Similarly

  $ cat spec_lines
  MYVAR=hello
  /bin/bash
  -c
  echo $MYVAR

  Any of the spec_* endpoints can be read or written, just like Linux /proc files.

  Note that in the above example, the second argument does not contain quotes.
  Those were interpreted by the spec_quoted endpoint to mean that "echo
  $MYVAR" was a single string that contained a space. Without them, it would
  have parsed three arguments.

  Escaped quotes and backslashes are understood by the parser.
  Adjacent quoted strings are appended, e.g., "hi"'th"ere' is parsed to be 'hith"ere'.

  Once prepared, you may control the process thus:

  echo start >> ctrl

  The ctrl command available are
    start: start a process (fails if running)
    reset: set process env, args, and binary to empty state (only if not running)
    kill [signal]: kill the process (only if running). Signal can be, e.g., -9,
        -HUP, 2, INT.

  The following are also available. Note that attempting to access any of these
  before the process starts will cause a file open operation to block until the
  process is ready.

  echo hello >> stdin        # Write to the standard input of the process.
  cat stdout                 # Read the standard output of the process.
  cat stderr                 # Read the standard error of the process.
  cat wait                   # Wait for process exit and read the exit code.

`

type Proc struct {
	sync.Mutex
	name string
	help string
	cmd  *exec.Cmd

	env []string
	bin string
	arg []string

	// The other ends of the std* pipes.
	stdinReader  iomisc.InterruptibleReader
	stdoutWriter iomisc.InterruptibleWriter
	stderrWriter iomisc.InterruptibleWriter

	specQuotedFID rh.FID
	specLinesFID  rh.FID
	specJsonFID   rh.FID
	ctrlFID       rh.FID
	helpFID       rh.FID
	statFID       rh.FID

	waitFID   rh.FID
	stdinFID  rh.FID
	stdoutFID rh.FID
	stderrFID rh.FID

	waitChan   chan error
	pipesReady chan bool
}

type procInfo struct {
	Env []string `json:"env"`
	Bin string   `json:"bin"`
	Arg []string `json:"arg"`
}

func NewProc(name string) (*Proc, rh.FID) {
	p := &Proc{
		name:       name,
		helpFID:    NewHelp(buildProcHelp(name, uint64(rand.Int63()))),
		waitChan:   make(chan error, 1),
		pipesReady: make(chan bool), // just close when ready
	}

	var inFID, outFID, errFID ns.UnregulatedFID

	// These pipes don't do anything initially. We hook them up to the process when it starts running.
	inFID, p.stdinReader = ns.NewPipeWriterFID("stdin")
	outFID, p.stdoutWriter = ns.NewPipeReaderFID("stdout")
	errFID, p.stderrWriter = ns.NewPipeReaderFID("stderr")

	p.stdinFID = ns.RegulateShareBlockUntilReady(inFID, ns.RegimeCloseOnGarbage, p.pipesReady)
	p.stdoutFID = ns.RegulateShare(outFID, ns.RegimeNeverClose)
	p.stderrFID = ns.RegulateShare(errFID, ns.RegimeNeverClose)

	p.waitFID = NewProcWait(p.waitChan)

	p.specQuotedFID = NewConfigFID(
		"spec_quoted",
		func(b []byte) error {
			parts, err := shargs.Split(string(b))
			if err != nil {
				return err
			}
			env, arg := shargs.DivideArgTypes(parts)
			if len(arg) < 1 {
				return fmt.Errorf("proc needs at least a binary, got %q", string(b))
			}
			return p.SetSpec(env, strings.TrimSpace(arg[0]), arg[1:])
		},
		func() []byte {
			env, bin, arg := p.Spec()
			var quoted []string
			for _, x := range env {
				quoted = append(quoted, shargs.Quote(x))
			}
			quoted = append(quoted, shargs.Quote(bin))
			for _, x := range arg {
				quoted = append(quoted, shargs.Quote(x))
			}
			return []byte(strings.Join(quoted, " "))
		},
	)

	p.specLinesFID = NewConfigFID(
		"spec_lines",
		func(b []byte) error {
			r := strings.NewReplacer(
				`\0`, "\x00",
				`\n`, "\n",
				`\r`, "\r",
				`\\`, `\`,
			)
			var lines []string
			buff := bytes.NewBuffer(b)
			scanner := bufio.NewScanner(buff)
			for scanner.Scan() {
				lines = append(lines, r.Replace(scanner.Text()))
			}
			env, arg := shargs.DivideArgTypes(lines)
			if len(arg) < 1 {
				return fmt.Errorf("proc needs at least a binary, got %q", string(b))
			}
			return p.SetSpec(env, strings.TrimSpace(arg[0]), arg[1:])
		},
		func() []byte {
			env, bin, arg := p.Spec()
			r := strings.NewReplacer(
				`\`, `\\`,
				"\r", `\r`,
				"\n", `\n`,
				"\x00", `\0`,
			)
			var lines []string
			for _, x := range env {
				lines = append(lines, r.Replace(x))
			}
			lines = append(lines, r.Replace(bin))
			for _, x := range arg {
				lines = append(lines, r.Replace(x))
			}
			return []byte(strings.Join(lines, "\n"))
		},
	)

	p.specJsonFID = NewConfigFID(
		"spec_json",
		func(b []byte) error {
			var info procInfo
			if err := json.Unmarshal(b, &info); err != nil {
				return fmt.Errorf("Error %v: invalid json sent to proc %q: %v", err, p.name, string(b))
			}
			return p.SetSpec(info.Env, info.Bin, info.Arg)
		},
		func() []byte {
			var info procInfo
			info.Env, info.Bin, info.Arg = p.Spec()
			if b, err := json.MarshalIndent(&info, "", "  "); err == nil {
				return b
			}
			return []byte{}
		},
	)

	p.ctrlFID = NewEndpoint(func(s string) {
		matches := ctrlExp.FindStringSubmatch(s)
		if matches == nil {
			// TODO(chris): surface errors somehow?
			log.Printf("Invalid control string %q", s)
			return
		}

		switch strings.ToLower(matches[1]) {
		case "start":
			if p.bin == "" {
				log.Printf("Error: no command to run in proc %v", p.name)
				return
			}
			if err := p.run(p.env, p.bin, p.arg...); err != nil {
				log.Printf("Error: %v", err)
				return
			}
		case "reset":
			if err := p.reset(); err != nil {
				log.Printf("Error: %v", err)
				return
			}
		case "kill":
			signumstr := matches[2]
			signamestr := matches[3]
			switch rs := p.runState(); rs {
			default:
				switch {
				case len(signumstr) > 0:
					n, _ := strconv.Atoi(signumstr) // regex ensures this will be a number
					p.cmd.Process.Signal(syscall.Signal(n))
				case len(signamestr) > 0:
					sig, ok := sigStrMap[signamestr]
					if !ok {
						log.Printf("Error - unknown signal name %s", signamestr)
						return
					}
					p.cmd.Process.Signal(sig)
				default:
					p.cmd.Process.Signal(os.Kill) // default to kill
				}
				return
			case Unknown:
				log.Printf("Process in unknown state: can't kill.")
				return
			case None, Signaled, Exited:
				log.Printf("No process to kill.")
				return
			}
			return
		default:
			// TODO(chris): surface errors like these somehow.
			log.Printf("Unknown command: %v", s)
		}
	})

	p.statFID = ns.RegulateFork(func() ns.UnregulatedFID {
		var w bytes.Buffer

		fmt.Fprintf(&w, "Cmd: %s\n", shargs.Quote(p.bin))

		qarg := make([]string, len(p.arg))
		for i, a := range p.arg {
			qarg[i] = shargs.Quote(a)
		}
		fmt.Fprintf(&w, "Arg: %s\n", strings.Join(qarg, " "))

		qenv := make([]string, len(p.env))
		for i, e := range p.env {
			qenv[i] = shargs.Quote(e)
		}
		fmt.Fprintf(&w, "Env: %s\n", strings.Join(qenv, " "))

		fmt.Fprintf(&w, "PID: ")
		if p.cmd != nil {
			fmt.Fprintf(&w, "%d", p.cmd.Process.Pid)
		}
		fmt.Fprintf(&w, "\n")

		fmt.Fprintf(&w, "Status: ")
		if p.cmd == nil {
			fmt.Fprintf(&w, "none")
		} else if ps, ws := p.state(); ps != nil {
			if ps.Exited() {
				fmt.Fprintf(&w, "exited %d ", ws.ExitStatus())
			}
			if ws.Stopped() {
				w.WriteString("stopped ")
			}
			if ws.Signaled() {
				sig := ws.Signal()
				fmt.Fprintf(&w, "signaled %q (%d)", sig.String(), sig)
			}
			if ws.Continued() {
				w.WriteString("continued ")
			}
		} else {
			w.WriteString("running ")
		}
		w.WriteByte('\n')
		r := bytes.NewReader(w.Bytes())
		return ns.NewReaderFID("proc/stat", iomisc.ReaderNopCloser(r))
	})

	return p, ns.NewFID("proc", &procDir{p})
}

func (p *Proc) SetSpec(env []string, bin string, args []string) error {
	defer un(lock(p))
	p.env = env
	p.bin = bin
	p.arg = args
	return nil
}

func (p *Proc) Spec() (env []string, bin string, args []string) {
	defer un(lock(p))
	return p.env, p.bin, p.arg
}

// reset makes all of the process arguments go away, including past run PIDs.
// This only works if the process is not currently running.
func (p *Proc) reset() error {
	defer un(lock(p))
	if p.isRunning() {
		return fmt.Errorf("Process %s cannot be reset while running", p.name)
	}
	p.cmd = nil
	p.bin = ""
	p.arg = []string{}
	p.env = []string{}
	return nil
}

func (p *Proc) runState() RunState {
	if p.cmd == nil {
		return None
	}
	ps, ws := p.state()
	switch {
	case ps == nil:
		return Running
	case ps.Exited():
		return Exited
	case ws.Stopped():
		return Stopped
	case ws.Signaled():
		return Signaled
	case ws.Continued():
		return Continued
	default:
		return Unknown
	}
}

func (p *Proc) isRunning() bool {
	rs := p.runState()
	return rs == Running || rs == Stopped || rs == Continued
}

// run starts the process with the given environment, command, and args.
// env is a list of x=y strings, where everything after the first = is treated
// as a single value.
func (p *Proc) run(env []string, bin string, arg ...string) error {
	defer un(lock(p))
	if p.isRunning() {
		return fmt.Errorf("Process %s is already running", p.name)
	}
	p.cmd = exec.Command(bin, arg...)
	if len(env) > 0 {
		p.cmd.Env = append(p.cmd.Env, os.Environ()...)
		p.cmd.Env = append(p.cmd.Env, env...)
	}

	// stdin, stdout, stderr FIDs
	stdin, err := p.cmd.StdinPipe()
	if err != nil {
		return err
	}
	stdout, err := p.cmd.StdoutPipe()
	if err != nil {
		return err
	}
	stderr, err := p.cmd.StderrPipe()
	if err != nil {
		return err
	}

	// Now we hook these up to the process via goroutines that just copy
	// everything to/from the actual process pipes from/to the fake internal
	// interruptible pipes:
	go func() {
		io.Copy(stdin, p.stdinReader)
		stdin.Close()
	}()

	go func() {
		io.Copy(p.stdoutWriter, stdout)
		p.stdoutWriter.Close()
	}()

	go func() {
		io.Copy(p.stderrWriter, stderr)
		p.stderrWriter.Close()
	}()

	// Start
	if err := p.cmd.Start(); err != nil {
		return err
	}

	// Signal the pipe files that we're ready and they can unblock.
	close(p.pipesReady)

	// wait FID
	// We can only call "Wait" once on a process, so we do just that, embodying
	// the result of the wait into a channel that multiple readers can use, but
	// that is written exactly once, on the single invocation of cmd.Wait().
	go func() {
		p.waitChan <- p.cmd.Wait()
		close(p.waitChan)
	}()

	return nil
}

func (p *Proc) state() (*os.ProcessState, syscall.WaitStatus) {
	if p.cmd == nil {
		return nil, 0
	}
	if ps := p.cmd.ProcessState; ps != nil {
		return ps, ps.Sys().(syscall.WaitStatus)
	}
	return nil, 0
}

// procDir is an ns.InterfaceDir
type procDir struct {
	p *Proc
}

func (pd *procDir) Help() rh.FID {
	return pd.p.helpFID
}

func (pd *procDir) Ctrl() rh.FID {
	return pd.p.ctrlFID
}

func (pd *procDir) Spec_quoted() rh.FID {
	return pd.p.specQuotedFID
}

func (pd *procDir) Spec_lines() rh.FID {
	return pd.p.specLinesFID
}

func (pd *procDir) Spec_json() rh.FID {
	return pd.p.specJsonFID
}

func (pd *procDir) Stdin() rh.FID {
	return pd.p.stdinFID
}

func (pd *procDir) Stdout() rh.FID {
	return pd.p.stdoutFID
}

func (pd *procDir) Stderr() rh.FID {
	return pd.p.stderrFID
}

func (pd *procDir) Wait() rh.FID {
	return pd.p.waitFID
}

func (pd *procDir) Stat() rh.FID {
	return pd.p.statFID
}
